import md4 from '@dove/md4';
import buffer from '@ohos.buffer';
import CryptoJS from '@ohos/crypto-js';
import NegotiateFlag from "./NegotiateFlag";

export const encodeNegotiationMessage = (hostname: string, domain: string) => {
  hostname = hostname.toUpperCase();
  domain = domain.toUpperCase();
  const hostnameLength = buffer.byteLength(hostname, "ascii");
  const domainLength = buffer.byteLength(domain, "ascii");
  let offset = 0;
  const buf = buffer.alloc(32 + hostnameLength + domainLength);
  buf.write("NTLMSSP", offset, 7, "ascii");
  offset += 7;
  buf.writeUInt8(0, offset);
  offset += 1;
  buf.writeUInt32LE(1, offset);
  offset += 4;
  const negotiateFlags = NegotiateFlag.UnicodeEncoding | NegotiateFlag.NTLMSessionSecurity | NegotiateFlag.AlwaysSign;
  buf.writeUInt32LE(negotiateFlags, offset);
  offset += 4;
  buf.writeUInt16LE(domainLength, offset);
  offset += 2;
  buf.writeUInt16LE(domainLength, offset);
  offset += 2;
  const domainOffset = 0x20 + hostnameLength;
  buf.writeUInt32LE(domainOffset, offset);
  offset += 4;
  buf.writeUInt16LE(hostnameLength, offset);
  offset += 2;
  buf.writeUInt16LE(hostnameLength, offset);
  offset += 2;
  buf.writeUInt32LE(0x20, offset);
  offset += 4;
  buf.write(hostname, 0x20, hostnameLength, "ascii");
  buf.write(domain, domainOffset, domainLength, "ascii");
  return buf;
};

export const decodeNegotiationMessage = (buf: buffer.Buffer) => {
  let offset = 0;
  const protocol = buf.subarray(0, 7).toString("ascii");
  if (protocol !== "NTLMSSP" || buf.readInt8(7) !== 0x00)
    throw new Error("ntlmssp_header_not_found");
  offset += 8;
  const type = buf.readUInt32LE(offset);
  if (type !== 0x01) throw new Error("ntlmssp_type_is_not_one");
  offset += 4;
  const negotiateFlags = buf.readUInt32LE(offset);
  offset += 4;
  const domainLength = buf.readUInt16LE(offset);
  offset += 2;
  const domainMaxLength = buf.readUInt16LE(offset);
  offset += 2;
  const domainOffset = buf.readUInt32LE(offset);
  offset += 4;
  const hostnameLength = buf.readUInt16LE(offset);
  offset += 2;
  const hostnameMaxLength = buf.readUInt16LE(offset);
  offset += 2;
  const hostnameOffset = buf.readUInt32LE(offset);
  offset += 4;
  const domain = buf.subarray(domainOffset, domainOffset + domainLength).toString("ascii");
  const hostname = buf.subarray(hostnameOffset, hostnameOffset + hostnameLength).toString("ascii");
  return { negotiateFlags, domain, hostname };
};

export const encodeChallengeMessage = (negotiateFlags: number) => {
  let offset = 0;
  const buf = buffer.alloc(64);
  buf.write("NTLMSSP", offset, 7, "ascii");
  offset += 7;
  buf.writeUInt8(0, offset);
  offset += 1;
  buf.writeUInt32LE(2, offset);
  offset += 4;
  buf.writeUInt16LE(0, offset);
  offset += 2;
  buf.writeUInt16LE(0, offset);
  offset += 2;
  buf.writeUInt32LE(0, offset);
  offset += 4;
  buf.writeUInt32LE(negotiateFlags, offset);
  offset += 4;
  generateServerChallenge().copy(buf, offset);
  offset += 8;
  buf.fill(0, offset, offset + 8);
  offset += 8;
  return buf;
};

export const decodeChallengeMessage = (buf: buffer.Buffer) => {
  let offset = 0;
  const protocol = buf.subarray(0, 7).toString("ascii");
  if (protocol !== "NTLMSSP" || buf.readInt8(7) !== 0x00)
    throw new Error("ntlmssp_header_not_found");
  offset += 8;
  const type = buf.readUInt32LE(offset);
  if (type !== 0x02) throw new Error("ntlmssp_type_is_not_two");
  offset += 4;
  const targetNameLength = buf.readUInt16LE(offset);
  offset += 2;
  const targetNameMaxLength = buf.readUInt16LE(offset);
  offset += 2;
  const targetNameOffset = buf.readUInt32LE(offset);
  offset += 4;
  const negotiateFlags = buf.readUInt32LE(offset);
  offset += 4;
  const serverChallenge = buf.subarray(offset, offset + 8);
  offset += 8;
  offset += 8;
  return serverChallenge;
}

export const encodeAuthenticationMessage = (username: string, hostname: string, domain: string, nonce: buffer.Buffer, password: string) => {
  hostname = hostname.toUpperCase();
  domain = domain.toUpperCase();
  const lmHash = buffer.alloc(21);
  createLmHash(password).copy(lmHash);
  lmHash.fill(0x00, 16);
  const ntHash = buffer.alloc(21);
  createNtHash(password).copy(ntHash);
  ntHash.fill(0x00, 16);
  const lmResponse = createResponse(lmHash, nonce);
  const ntResponse = createResponse(ntHash, nonce);
  const usernameLength = buffer.byteLength(username, "ucs2");
  const hostnameLength = buffer.byteLength(hostname, "ucs2");
  const domainLength = buffer.byteLength(domain, "ucs2");
  const lmResponseLength = 0x18;
  const ntResponseLength = 0x18;
  const domainOffset = 0x40;
  const usernameOffset = domainOffset + domainLength;
  const hostnameOffset = usernameOffset + usernameLength;
  const lmResponseOffset = hostnameOffset + hostnameLength;
  const ntResponseOffset = lmResponseOffset + lmResponseLength;
  let offset = 0;
  const messageLength = 64 + domainLength + usernameLength + hostnameLength + lmResponseLength + ntResponseLength;
  const buf = buffer.alloc(messageLength);
  buf.write("NTLMSSP", offset, 7, "ascii");
  offset += 7;
  buf.writeUInt8(0, offset);
  offset++;
  buf.writeUInt8(0x03, offset);
  offset++;
  buf.fill(0x00, offset, offset + 3);
  offset += 3;
  buf.writeUInt16LE(lmResponseLength, offset);
  offset += 2;
  buf.writeUInt16LE(lmResponseLength, offset);
  offset += 2;
  buf.writeUInt16LE(lmResponseOffset, offset);
  offset += 2;
  buf.fill(0x00, offset, offset + 2);
  offset += 2;
  buf.writeUInt16LE(ntResponseLength, offset);
  offset += 2;
  buf.writeUInt16LE(ntResponseLength, offset);
  offset += 2;
  buf.writeUInt16LE(ntResponseOffset, offset);
  offset += 2;
  buf.fill(0x00, offset, offset + 2);
  offset += 2;
  buf.writeUInt16LE(domainLength, offset);
  offset += 2;
  buf.writeUInt16LE(domainLength, offset);
  offset += 2;
  buf.writeUInt16LE(domainOffset, offset);
  offset += 2;
  buf.fill(0x00, offset, offset + 2);
  offset += 2;
  buf.writeUInt16LE(usernameLength, offset);
  offset += 2;
  buf.writeUInt16LE(usernameLength, offset);
  offset += 2;
  buf.writeUInt16LE(usernameOffset, offset);
  offset += 2;
  buf.fill(0x00, offset, offset + 2);
  offset += 2;
  buf.writeUInt16LE(hostnameLength, offset);
  offset += 2;
  buf.writeUInt16LE(hostnameLength, offset);
  offset += 2;
  buf.writeUInt16LE(hostnameOffset, offset);
  offset += 2;
  buf.fill(0x00, offset, offset + 6);
  offset += 6;
  buf.writeUInt16LE(messageLength, offset);
  offset += 2;
  buf.fill(0x00, offset, offset + 2);
  offset += 2;
  const negotiateFlags = NegotiateFlag.UnicodeEncoding | NegotiateFlag.NTLMSessionSecurity | NegotiateFlag.AlwaysSign;
  buf.writeUInt32LE(negotiateFlags, offset);
  offset += 4;
  buf.write(domain, domainOffset, domainLength, "ucs2");
  buf.write(username, usernameOffset, usernameLength, "ucs2");
  buf.write(hostname, hostnameOffset, hostnameLength, "ucs2");
  lmResponse.copy(buf, lmResponseOffset, 0, lmResponseLength);
  ntResponse.copy(buf, ntResponseOffset, 0, ntResponseLength);
  return buf;
};

export const generateServerChallenge = () => {
  const words = CryptoJS.lib.WordArray.random(8).words;
  return buffer.from(words);
};

const fixOddParity = (buf: buffer.Buffer) => {
  for (let index = 0; index < buf.length; index++) {
    let parity = 1;
    for (let index2 = 1; index2 < 8; index2++)
      parity = (parity + ((buf[index] >> index2) & 1)) % 2;
    buf[index] |= parity & 1;
  }
  return buf;
};

const createDESKey = (key56: buffer.Buffer) => {
  const key64 = buffer.alloc(8);
  key64[0] = key56[0] & 0xFE;
  key64[1] = ((key56[0] << 7) & 0xFF) | (key56[1] >> 1);
  key64[2] = ((key56[1] << 6) & 0xFF) | (key56[2] >> 2);
  key64[3] = ((key56[2] << 5) & 0xFF) | (key56[3] >> 3);
  key64[4] = ((key56[3] << 4) & 0xFF) | (key56[4] >> 4);
  key64[5] = ((key56[4] << 3) & 0xFF) | (key56[5] >> 5);
  key64[6] = ((key56[5] << 2) & 0xFF) | (key56[6] >> 6);
  key64[7] = (key56[6] << 1) & 0xFF;
  return key64;
};

const createLmHash = (text: string) => {
  const upperCaseText = text.substring(0, 14).toUpperCase();
  const upperCaseTextLength = buffer.byteLength(upperCaseText, "ascii");
  const paddingBuffer = buffer.alloc(14);
  paddingBuffer.write(upperCaseText, 0, upperCaseTextLength, "ascii");
  paddingBuffer.fill(0, upperCaseTextLength);
  const halves = [fixOddParity(createDESKey(paddingBuffer.subarray(0, 7))),
  fixOddParity(createDESKey(paddingBuffer.subarray(7, 14)))];
  const buf = buffer.alloc(16);
  let offset = 0;
  for (const key of halves) {
    const halve = CryptoJS.enc.Hex.parse(key.toString('hex'));
    const string = CryptoJS.DES.encrypt("KGS!@#$%", halve, {
      mode: CryptoJS.mode.ECB,
      padding: CryptoJS.pad.NoPadding,
      iv: CryptoJS.lib.WordArray.create([])
    }).toString();
    buf.write(string, offset, offset + 8, "binary");
    offset += 8;
  }
  return buf;
};

const createNtHash = (str: string) => {
  const ucs2 = buffer.from(str, "ucs2");
  const md = new md4();
  md.update(ucs2);
  return buffer.from(md.hex(), 'hex');
};

const createResponse = (hash: buffer.Buffer, nonce: buffer.Buffer) => {
  const buf = buffer.alloc(24);
  for (let index = 0; index < 3; index++) {
    const keyBuffer = fixOddParity(createDESKey(hash.subarray(index * 7, index * 7 + 7)));
    const key = CryptoJS.lib.WordArray.create(keyBuffer.buffer);
    const string = CryptoJS.DES.encrypt(nonce.toString("binary"), key, {
      mode: CryptoJS.mode.ECB,
      padding: CryptoJS.pad.NoPadding
    });
    buf.write(string.toString(), index * 8, index * 8 + 8, "binary");
  }
  return buf;
};